mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 00:46:20 +01:00
Compare commits
12 Commits
shawn/mcps
...
shawn/comS
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c55166414 | ||
|
|
ec136d36ff | ||
|
|
68e98d62aa | ||
|
|
6e8142929e | ||
|
|
55593e5b99 | ||
|
|
8d8d451811 | ||
|
|
d06555db93 | ||
|
|
baf42abba4 | ||
|
|
c818859103 | ||
|
|
34a06715a8 | ||
|
|
e4a48b85a1 | ||
|
|
804de07b20 |
12
.vscode/mcp.json
vendored
Normal file
12
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"servers": {
|
||||
"powertoys-mcp": {
|
||||
"command": "D:/work/PowerToys/x64/Debug/PowerToys.MCPServer.exe",
|
||||
"transport": "stdio",
|
||||
"args": [],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/common/ManagedCommon/ErrorResponse.cs
Normal file
24
src/common/ManagedCommon/ErrorResponse.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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class ErrorResponse
|
||||
{
|
||||
public string Error { get; set; } = string.Empty;
|
||||
|
||||
public IEnumerable<string>? RegisteredModules { get; set; }
|
||||
|
||||
public string[]? AvailableEndpoints { get; set; }
|
||||
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
26
src/common/ManagedCommon/GlobalStatusResponse.cs
Normal file
26
src/common/ManagedCommon/GlobalStatusResponse.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class GlobalStatusResponse
|
||||
{
|
||||
public string Application { get; set; } = string.Empty;
|
||||
|
||||
public string? Version { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public int RegisteredModules { get; set; }
|
||||
|
||||
public Dictionary<string, ModuleStatusResponse> Modules { get; set; } = [];
|
||||
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
358
src/common/ManagedCommon/HttpServer.cs
Normal file
358
src/common/ManagedCommon/HttpServer.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class HttpServer : IDisposable
|
||||
{
|
||||
private readonly HttpListener _listener;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Dictionary<string, IHttpRequestHandler> _requestHandlers;
|
||||
private readonly JsonSerializerOptions _fallbackJsonOptions;
|
||||
private Task? _listenerTask;
|
||||
private bool _disposed;
|
||||
|
||||
public HttpServer(string prefix = "http://localhost:8080/")
|
||||
{
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add(prefix);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_requestHandlers = new Dictionary<string, IHttpRequestHandler>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Cached fallback options for generic deserialization
|
||||
_fallbackJsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
Logger.LogInfo($"HTTP server configured to listen on: {prefix}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a request handler for a specific module.
|
||||
/// </summary>
|
||||
/// <param name="handler">The request handler to register.</param>
|
||||
public void RegisterHandler(IHttpRequestHandler handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(handler.ModuleName))
|
||||
{
|
||||
throw new ArgumentException("Module name cannot be null or empty", nameof(handler));
|
||||
}
|
||||
|
||||
_requestHandlers[handler.ModuleName] = handler;
|
||||
Logger.LogInfo($"Registered HTTP handler for module: {handler.ModuleName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a request handler for a specific module.
|
||||
/// </summary>
|
||||
/// <param name="moduleName">The name of the module to unregister.</param>
|
||||
public void UnregisterHandler(string moduleName)
|
||||
{
|
||||
if (_requestHandlers.Remove(moduleName))
|
||||
{
|
||||
Logger.LogInfo($"Unregistered HTTP handler for module: {moduleName}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_listener.Start();
|
||||
Logger.LogInfo("HTTP server started successfully");
|
||||
|
||||
_listenerTask = Task.Run(async () => await ListenAsync(_cancellationTokenSource.Token));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_listener.Stop();
|
||||
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
|
||||
Logger.LogInfo("HTTP server stopped");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error stopping HTTP server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility method for modules to write JSON responses.
|
||||
/// </summary>
|
||||
/// <param name="response">The HTTP response to write to.</param>
|
||||
/// <param name="data">The object to serialize as JSON.</param>
|
||||
public async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
|
||||
{
|
||||
response.ContentType = "application/json";
|
||||
response.ContentEncoding = Encoding.UTF8;
|
||||
|
||||
string json = data switch
|
||||
{
|
||||
ServerStatusResponse serverStatus => JsonSerializer.Serialize(serverStatus, HttpServerJsonContext.Default.ServerStatusResponse),
|
||||
GlobalStatusResponse globalStatus => JsonSerializer.Serialize(globalStatus, HttpServerJsonContext.Default.GlobalStatusResponse),
|
||||
ErrorResponse error => JsonSerializer.Serialize(error, HttpServerJsonContext.Default.ErrorResponse),
|
||||
_ => JsonSerializer.Serialize(data, HttpServerJsonContext.Default.Object),
|
||||
};
|
||||
|
||||
var buffer = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
response.ContentLength64 = buffer.Length;
|
||||
await response.OutputStream.WriteAsync(buffer.AsMemory(), _cancellationTokenSource.Token);
|
||||
response.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility method for modules to read JSON request bodies as string.
|
||||
/// Modules should handle their own deserialization to maintain AOT compatibility.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request to read from.</param>
|
||||
/// <returns>The JSON string, or null if no body.</returns>
|
||||
public async Task<string?> ReadJsonRequestBodyAsync(HttpListenerRequest request)
|
||||
{
|
||||
if (!request.HasEntityBody)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(request.InputStream, Encoding.UTF8);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
return string.IsNullOrWhiteSpace(body) ? null : body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy utility method for modules to read JSON request bodies.
|
||||
/// Warning: This method uses reflection-based deserialization and is not AOT-compatible.
|
||||
/// Consider using ReadJsonRequestBodyAsync and handling deserialization in your module.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||
/// <param name="request">The HTTP request to read from.</param>
|
||||
/// <returns>The deserialized object, or null if no body or invalid JSON.</returns>
|
||||
[Obsolete("Use ReadJsonRequestBodyAsync and handle deserialization in your module for AOT compatibility")]
|
||||
public async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
|
||||
where T : class
|
||||
{
|
||||
var body = await ReadJsonRequestBodyAsync(request);
|
||||
if (body == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling
|
||||
return JsonSerializer.Deserialize<T>(body, _fallbackJsonOptions);
|
||||
#pragma warning restore IL3050
|
||||
#pragma warning restore IL2026
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Logger.LogError($"Error deserializing request body: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
Stop();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_listener?.Close();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && _listener.IsListening)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contextTask = _listener.GetContextAsync();
|
||||
var context = await contextTask.ConfigureAwait(false);
|
||||
|
||||
// Handle request asynchronously without blocking the listener
|
||||
_ = Task.Run(async () => await HandleRequestAsync(context), cancellationToken);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Expected when listener is stopped
|
||||
break;
|
||||
}
|
||||
catch (HttpListenerException ex) when (ex.ErrorCode == 995)
|
||||
{
|
||||
// Expected when listener is stopped
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error in HTTP listener: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRequestAsync(HttpListenerContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
|
||||
Logger.LogInfo($"HTTP Request: {request.HttpMethod} {request.Url?.AbsolutePath}");
|
||||
|
||||
// Set CORS headers
|
||||
response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (request.HttpMethod == "OPTIONS")
|
||||
{
|
||||
response.StatusCode = 200;
|
||||
response.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var path = request.Url?.AbsolutePath?.TrimStart('/');
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
await HandleRootRequestAsync(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the path to extract module name and sub-path
|
||||
var segments = path.Split('/', 2);
|
||||
var moduleName = segments[0];
|
||||
var subPath = segments.Length > 1 ? segments[1] : string.Empty;
|
||||
|
||||
// Check for global endpoints
|
||||
if (string.Equals(moduleName, "status", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await HandleGlobalStatusAsync(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Route to module-specific handler
|
||||
if (_requestHandlers.TryGetValue(moduleName, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.HandleRequestAsync(context, subPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error in module handler for {moduleName}: {ex.Message}");
|
||||
await HandleErrorAsync(response, 500, $"Internal server error in {moduleName} module: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleNotFoundAsync(response, moduleName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Unhandled error in HTTP request handler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRootRequestAsync(HttpListenerResponse response)
|
||||
{
|
||||
var rootInfo = new ServerStatusResponse
|
||||
{
|
||||
Application = "PowerToys HTTP Server",
|
||||
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
|
||||
Status = "Running",
|
||||
RegisteredModules = _requestHandlers.Keys,
|
||||
AvailableEndpoints = [
|
||||
"GET /status - Get server status",
|
||||
"GET /{module}/... - Module-specific endpoints",
|
||||
],
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
await WriteJsonResponseAsync(response, rootInfo);
|
||||
}
|
||||
|
||||
private async Task HandleGlobalStatusAsync(HttpListenerResponse response)
|
||||
{
|
||||
var moduleStatuses = new Dictionary<string, ModuleStatusResponse>();
|
||||
|
||||
foreach (var kvp in _requestHandlers)
|
||||
{
|
||||
moduleStatuses[kvp.Key] = new ModuleStatusResponse
|
||||
{
|
||||
ModuleName = kvp.Value.ModuleName,
|
||||
AvailableEndpoints = kvp.Value.GetAvailableEndpoints(),
|
||||
};
|
||||
}
|
||||
|
||||
var status = new GlobalStatusResponse
|
||||
{
|
||||
Application = "PowerToys HTTP Server",
|
||||
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
|
||||
Status = "Running",
|
||||
RegisteredModules = _requestHandlers.Count,
|
||||
Modules = moduleStatuses,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
await WriteJsonResponseAsync(response, status);
|
||||
}
|
||||
|
||||
private async Task HandleNotFoundAsync(HttpListenerResponse response, string requestedModule)
|
||||
{
|
||||
var errorResponse = new ErrorResponse
|
||||
{
|
||||
Error = $"Module '{requestedModule}' not found",
|
||||
RegisteredModules = _requestHandlers.Keys,
|
||||
AvailableEndpoints = [
|
||||
"GET /status - Get server status and available modules",
|
||||
],
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonResponseAsync(response, errorResponse);
|
||||
}
|
||||
|
||||
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
|
||||
{
|
||||
response.StatusCode = statusCode;
|
||||
var errorResponse = new ErrorResponse
|
||||
{
|
||||
Error = message,
|
||||
StatusCode = statusCode,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
await WriteJsonResponseAsync(response, errorResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/common/ManagedCommon/HttpServerJsonContext.cs
Normal file
23
src/common/ManagedCommon/HttpServerJsonContext.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
[JsonSerializable(typeof(ServerStatusResponse))]
|
||||
[JsonSerializable(typeof(ModuleStatusResponse))]
|
||||
[JsonSerializable(typeof(GlobalStatusResponse))]
|
||||
[JsonSerializable(typeof(ErrorResponse))]
|
||||
[JsonSerializable(typeof(Dictionary<string, ModuleStatusResponse>))]
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
[JsonSerializable(typeof(IEnumerable<string>))]
|
||||
[JsonSerializable(typeof(object))]
|
||||
internal sealed partial class HttpServerJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
36
src/common/ManagedCommon/IHttpRequestHandler.cs
Normal file
36
src/common/ManagedCommon/IHttpRequestHandler.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public interface IHttpRequestHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle an HTTP request for this module.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context containing request and response.</param>
|
||||
/// <param name="path">The requested path (after module prefix).</param>
|
||||
/// <returns>Task representing the async operation.</returns>
|
||||
Task HandleRequestAsync(HttpListenerContext context, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Get the module name used for URL routing (e.g., "awake", "fancyzones").
|
||||
/// </summary>
|
||||
string ModuleName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the available endpoints for this module (for documentation).
|
||||
/// </summary>
|
||||
string[] GetAvailableEndpoints();
|
||||
}
|
||||
}
|
||||
15
src/common/ManagedCommon/ModuleStatusResponse.cs
Normal file
15
src/common/ManagedCommon/ModuleStatusResponse.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class ModuleStatusResponse
|
||||
{
|
||||
public string ModuleName { get; set; } = string.Empty;
|
||||
|
||||
public string[] AvailableEndpoints { get; set; } = [];
|
||||
}
|
||||
}
|
||||
225
src/common/ManagedCommon/RotSingletonHost.cs
Normal file
225
src/common/ManagedCommon/RotSingletonHost.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/common/ManagedCommon/ServerStatusResponse.cs
Normal file
26
src/common/ManagedCommon/ServerStatusResponse.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public sealed class ServerStatusResponse
|
||||
{
|
||||
public string Application { get; set; } = string.Empty;
|
||||
|
||||
public string? Version { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public IEnumerable<string> RegisteredModules { get; set; } = [];
|
||||
|
||||
public string[] AvailableEndpoints { get; set; } = [];
|
||||
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@@ -21,6 +23,133 @@ namespace PowerToys.MCPServer.Tools
|
||||
[Description("Echoes the message back to the client.")]
|
||||
public static string SetTimeTest(string message) => $"Hello {message}";
|
||||
|
||||
// =============================
|
||||
// HTTP client (Awake remote control)
|
||||
// =============================
|
||||
private static readonly HttpClient _http = new HttpClient();
|
||||
|
||||
// Base URL for Awake HTTP server. Default matches Awake --http-port default (8080).
|
||||
// Allow override through environment variable POWERTOYS_AWAKE_HTTP (e.g. http://localhost:9090/)
|
||||
private static string BaseUrl => (Environment.GetEnvironmentVariable("POWERTOYS_AWAKE_HTTP") ?? "http://localhost:8080/").TrimEnd('/') + "/";
|
||||
|
||||
private static string JsonError(string msg, int? status = null) => JsonSerializer.Serialize(new { success = false, error = msg, status });
|
||||
|
||||
private static string JsonOk(object payload) => JsonSerializer.Serialize(payload);
|
||||
|
||||
private static string SendAwakeRequest(string method, string relativePath, object? body = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var req = new HttpRequestMessage(new HttpMethod(method), BaseUrl + relativePath.TrimStart('/'));
|
||||
if (body != null)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(body);
|
||||
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
using var resp = _http.Send(req);
|
||||
string respText = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
return JsonError($"HTTP {(int)resp.StatusCode} {resp.StatusCode}", (int)resp.StatusCode) + "\n" + respText;
|
||||
}
|
||||
|
||||
return respText;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return JsonError($"Connection failed: {ex.Message}. Ensure Awake is running with --http-server.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("RECOMMENDED FOR BUILDS: Intelligently keeps the system awake during build, compile, download, or process operations. Monitors CPU/memory/network activity and prevents sleep only when system is actively working. Automatically returns to sleep when build completes. PREFERRED over indefinite mode for development workflows. Params: cpuThresholdPercent (0-100), memThresholdPercent (0-100), netThresholdKBps (KB/s), sampleIntervalSeconds (>0), inactivityTimeoutSeconds (>0), keepDisplayOn=true|false")]
|
||||
public static string AwakeHttpActivityBased(uint cpuThresholdPercent = 50, uint memThresholdPercent = 50, uint netThresholdKBps = 10, uint sampleIntervalSeconds = 30, uint inactivityTimeoutSeconds = 300, bool keepDisplayOn = true)
|
||||
{
|
||||
if (cpuThresholdPercent > 100)
|
||||
{
|
||||
return JsonError("cpuThresholdPercent must be 0-100");
|
||||
}
|
||||
|
||||
if (memThresholdPercent > 100)
|
||||
{
|
||||
return JsonError("memThresholdPercent must be 0-100");
|
||||
}
|
||||
|
||||
if (sampleIntervalSeconds == 0)
|
||||
{
|
||||
return JsonError("sampleIntervalSeconds must be > 0");
|
||||
}
|
||||
|
||||
if (inactivityTimeoutSeconds == 0)
|
||||
{
|
||||
return JsonError("inactivityTimeoutSeconds must be > 0");
|
||||
}
|
||||
|
||||
return SendAwakeRequest("POST", "awake/activity", new
|
||||
{
|
||||
cpuThresholdPercent,
|
||||
memThresholdPercent,
|
||||
netThresholdKBps,
|
||||
sampleIntervalSeconds,
|
||||
inactivityTimeoutSeconds,
|
||||
keepDisplayOn,
|
||||
});
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Get Awake HTTP status (GET /awake/status). Requires Awake launched with --http-server.")]
|
||||
public static string AwakeHttpStatus() => SendAwakeRequest("GET", "awake/status");
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Keeps the system awake indefinitely until manually changed. WARNING: Less efficient than activity-based mode for builds. Use only when you need guaranteed continuous awake state regardless of system activity. Params: keepDisplayOn=true|false, processId=0")]
|
||||
public static string AwakeHttpIndefinite(bool keepDisplayOn = true, int processId = 0)
|
||||
=> SendAwakeRequest("POST", "awake/indefinite", new { keepDisplayOn, processId });
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Set timed keep-awake via HTTP. Params: seconds (>0), keepDisplayOn=true|false")]
|
||||
public static string AwakeHttpTimed(uint seconds, bool keepDisplayOn = true)
|
||||
{
|
||||
if (seconds == 0)
|
||||
{
|
||||
return JsonError("seconds must be > 0");
|
||||
}
|
||||
|
||||
return SendAwakeRequest("POST", "awake/timed", new { seconds, keepDisplayOn });
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Set expirable keep-awake via HTTP. Params: expireAt (ISO 8601), keepDisplayOn=true|false")]
|
||||
public static string AwakeHttpExpirable(string expireAt, bool keepDisplayOn = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expireAt))
|
||||
{
|
||||
return JsonError("expireAt required (ISO 8601)");
|
||||
}
|
||||
|
||||
return SendAwakeRequest("POST", "awake/expirable", new { expireAt, keepDisplayOn });
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Set passive mode via HTTP (POST /awake/passive).")]
|
||||
public static string AwakeHttpPassive() => SendAwakeRequest("POST", "awake/passive");
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Toggle display keep-on via HTTP (POST /awake/display/toggle).")]
|
||||
public static string AwakeHttpToggleDisplay() => SendAwakeRequest("POST", "awake/display/toggle");
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Get Awake settings via HTTP (GET /awake/settings).")]
|
||||
public static string AwakeHttpSettings() => SendAwakeRequest("GET", "awake/settings");
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Check current PowerToys Awake mode and configuration. Returns active mode (indefinite, timed, activity-based, or passive), remaining time, thresholds, and display settings. Use to verify if system is being kept awake and what settings are active.")]
|
||||
public static string AwakeHttpConfig() => SendAwakeRequest("GET", "awake/config");
|
||||
|
||||
private sealed class AppUsageRecord
|
||||
{
|
||||
[JsonPropertyName("process")]
|
||||
@@ -53,7 +182,6 @@ namespace PowerToys.MCPServer.Tools
|
||||
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");
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
<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" />
|
||||
<!-- 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>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<UseWindowsForms>False</UseWindowsForms>
|
||||
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
|
||||
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
|
||||
<AssemblyName>PowerToys.Awake</AssemblyName>
|
||||
|
||||
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
|
||||
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<UseWindowsForms>False</UseWindowsForms>
|
||||
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
|
||||
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
|
||||
<AssemblyName>PowerToys.Awake</AssemblyName>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
|
||||
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Awake\Awake.ico" />
|
||||
<None Remove="Assets\Awake\disabled.ico" />
|
||||
<None Remove="Assets\Awake\expirable.ico" />
|
||||
<None Remove="Assets\Awake\indefinite.ico" />
|
||||
<None Remove="Assets\Awake\normal.ico" />
|
||||
<None Remove="Assets\Awake\scheduled.ico" />
|
||||
<None Remove="Assets\Awake\timed.ico" />
|
||||
</ItemGroup>
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Runtime.Caching" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Awake\Awake.ico" />
|
||||
<None Remove="Assets\Awake\disabled.ico" />
|
||||
<None Remove="Assets\Awake\expirable.ico" />
|
||||
<None Remove="Assets\Awake\indefinite.ico" />
|
||||
<None Remove="Assets\Awake\normal.ico" />
|
||||
<None Remove="Assets\Awake\scheduled.ico" />
|
||||
<None Remove="Assets\Awake\timed.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
<PackageReference Include="System.Runtime.Caching" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Program.cs">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Program.cs">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\Awake\Awake.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\disabled.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\expirable.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\indefinite.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\normal.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\scheduled.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\timed.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\Awake\Awake.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\disabled.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\expirable.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\indefinite.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\normal.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\scheduled.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Assets\Awake\timed.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
33
src/modules/awake/Awake/AwakeAutomation.cs
Normal file
33
src/modules/awake/Awake/AwakeAutomation.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
402
src/modules/awake/Awake/Core/AwakeHttpHandler.cs
Normal file
402
src/modules/awake/Awake/Core/AwakeHttpHandler.cs
Normal file
@@ -0,0 +1,402 @@
|
||||
// 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.Globalization;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Awake.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP request handler for Awake module functionality.
|
||||
/// </summary>
|
||||
internal sealed class AwakeHttpHandler : IHttpRequestHandler
|
||||
{
|
||||
public string ModuleName => "awake";
|
||||
|
||||
public string[] GetAvailableEndpoints()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
"GET /awake/status - Get current Awake status",
|
||||
"GET /awake/config - Get current Awake configuration",
|
||||
"POST /awake/indefinite - Set indefinite keep-awake (body: {\"keepDisplayOn\": true, \"processId\": 0})",
|
||||
"POST /awake/timed - Set timed keep-awake (body: {\"seconds\": 3600, \"keepDisplayOn\": true})",
|
||||
"POST /awake/expirable - Set expirable keep-awake (body: {\"expireAt\": \"2024-12-31T23:59:59Z\", \"keepDisplayOn\": true})",
|
||||
"POST /awake/activity - Set activity-based keep-awake (body: {\"cpuThresholdPercent\": 50, \"memThresholdPercent\": 50, \"netThresholdKBps\": 10, \"sampleIntervalSeconds\": 30, \"inactivityTimeoutSeconds\": 300, \"keepDisplayOn\": true})",
|
||||
"POST /awake/passive - Set passive mode (no keep-awake)",
|
||||
"POST /awake/display/toggle - Toggle display setting",
|
||||
"GET /awake/settings - Get current PowerToys settings",
|
||||
};
|
||||
}
|
||||
|
||||
public async Task HandleRequestAsync(HttpListenerContext context, string path)
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var method = request.HttpMethod.ToUpperInvariant();
|
||||
var pathLower = path.ToLowerInvariant();
|
||||
|
||||
try
|
||||
{
|
||||
switch ((method, pathLower))
|
||||
{
|
||||
case ("GET", "status"):
|
||||
await HandleGetStatusAsync(response);
|
||||
break;
|
||||
|
||||
case ("GET", "config"):
|
||||
await HandleGetConfigAsync(response);
|
||||
break;
|
||||
|
||||
case ("POST", "indefinite"):
|
||||
await HandleSetIndefiniteAwakeAsync(request, response);
|
||||
break;
|
||||
|
||||
case ("POST", "timed"):
|
||||
await HandleSetTimedAwakeAsync(request, response);
|
||||
break;
|
||||
|
||||
case ("POST", "expirable"):
|
||||
await HandleSetExpirableAwakeAsync(request, response);
|
||||
break;
|
||||
|
||||
case ("POST", "activity"):
|
||||
await HandleSetActivityBasedAwakeAsync(request, response);
|
||||
break;
|
||||
|
||||
case ("POST", "passive"):
|
||||
await HandleSetPassiveAwakeAsync(response);
|
||||
break;
|
||||
|
||||
case ("POST", "display/toggle"):
|
||||
await HandleToggleDisplayAsync(response);
|
||||
break;
|
||||
|
||||
case ("GET", "settings"):
|
||||
await HandleGetSettingsAsync(response);
|
||||
break;
|
||||
|
||||
default:
|
||||
await HandleNotFoundAsync(response, path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error handling Awake request {method} /{path}: {ex.Message}");
|
||||
await HandleErrorAsync(response, 500, $"Internal server error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleGetStatusAsync(HttpListenerResponse response)
|
||||
{
|
||||
var status = new
|
||||
{
|
||||
Module = "Awake",
|
||||
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
|
||||
Status = "Running",
|
||||
UsingPowerToysConfig = Manager.IsUsingPowerToysConfig,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
await WriteJsonResponseAsync(response, status);
|
||||
}
|
||||
|
||||
private async Task HandleGetConfigAsync(HttpListenerResponse response)
|
||||
{
|
||||
var config = Manager.GetCurrentConfig();
|
||||
await WriteJsonResponseAsync(response, config);
|
||||
}
|
||||
|
||||
private async Task HandleSetIndefiniteAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var requestData = await ReadJsonRequestAsync<IndefiniteAwakeRequest>(request);
|
||||
|
||||
Manager.SetIndefiniteKeepAwake(
|
||||
requestData?.KeepDisplayOn ?? true,
|
||||
requestData?.ProcessId ?? 0,
|
||||
"HttpServer");
|
||||
|
||||
var result = new
|
||||
{
|
||||
Success = true,
|
||||
Mode = "Indefinite",
|
||||
KeepDisplayOn = requestData?.KeepDisplayOn ?? true,
|
||||
ProcessId = requestData?.ProcessId ?? 0,
|
||||
};
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleSetTimedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var requestData = await ReadJsonRequestAsync<TimedAwakeRequest>(request);
|
||||
|
||||
if (requestData?.Seconds == null || requestData.Seconds <= 0)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid or missing 'seconds' parameter. Must be a positive integer.");
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetTimedKeepAwake(
|
||||
requestData.Seconds,
|
||||
requestData.KeepDisplayOn ?? true,
|
||||
"HttpServer");
|
||||
|
||||
var result = new
|
||||
{
|
||||
Success = true,
|
||||
Mode = "Timed",
|
||||
Seconds = requestData.Seconds,
|
||||
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
|
||||
};
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleSetExpirableAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var requestData = await ReadJsonRequestAsync<ExpirableAwakeRequest>(request);
|
||||
|
||||
if (requestData?.ExpireAt == null)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Missing 'expireAt' parameter. Expected ISO 8601 format (e.g., '2024-12-31T23:59:59Z').");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestData.ExpireAt <= DateTimeOffset.Now)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Expiration time must be in the future.");
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetExpirableKeepAwake(
|
||||
requestData.ExpireAt,
|
||||
requestData.KeepDisplayOn ?? true,
|
||||
"HttpServer");
|
||||
|
||||
var result = new
|
||||
{
|
||||
Success = true,
|
||||
Mode = "Expirable",
|
||||
ExpireAt = requestData.ExpireAt,
|
||||
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
|
||||
};
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleSetActivityBasedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
var requestData = await ReadJsonRequestAsync<ActivityBasedAwakeRequest>(request);
|
||||
|
||||
if (requestData == null)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid or missing request body. Expected JSON with cpuThresholdPercent, memThresholdPercent, netThresholdKBps, sampleIntervalSeconds, inactivityTimeoutSeconds, and optional keepDisplayOn.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (requestData.CpuThresholdPercent > 100)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid cpuThresholdPercent. Must be between 0 and 100.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestData.MemThresholdPercent > 100)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid memThresholdPercent. Must be between 0 and 100.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestData.SampleIntervalSeconds == 0)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid sampleIntervalSeconds. Must be greater than 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestData.InactivityTimeoutSeconds == 0)
|
||||
{
|
||||
await HandleErrorAsync(response, 400, "Invalid inactivityTimeoutSeconds. Must be greater than 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
Manager.SetActivityBasedKeepAwake(
|
||||
requestData.CpuThresholdPercent,
|
||||
requestData.MemThresholdPercent,
|
||||
requestData.NetThresholdKBps,
|
||||
requestData.SampleIntervalSeconds,
|
||||
requestData.InactivityTimeoutSeconds,
|
||||
requestData.KeepDisplayOn ?? true,
|
||||
"HttpServer");
|
||||
|
||||
var result = new
|
||||
{
|
||||
Success = true,
|
||||
Mode = "Activity-based",
|
||||
CpuThresholdPercent = requestData.CpuThresholdPercent,
|
||||
MemThresholdPercent = requestData.MemThresholdPercent,
|
||||
NetThresholdKBps = requestData.NetThresholdKBps,
|
||||
SampleIntervalSeconds = requestData.SampleIntervalSeconds,
|
||||
InactivityTimeoutSeconds = requestData.InactivityTimeoutSeconds,
|
||||
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
|
||||
};
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleSetPassiveAwakeAsync(HttpListenerResponse response)
|
||||
{
|
||||
Manager.SetPassiveKeepAwake(true, "HttpServer");
|
||||
|
||||
var result = new { Success = true, Mode = "Passive" };
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleToggleDisplayAsync(HttpListenerResponse response)
|
||||
{
|
||||
Manager.SetDisplay("HttpServer");
|
||||
|
||||
var result = new { Success = true, Action = "Display setting toggled" };
|
||||
await WriteJsonResponseAsync(response, result);
|
||||
}
|
||||
|
||||
private async Task HandleGetSettingsAsync(HttpListenerResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Manager.ModuleSettings != null)
|
||||
{
|
||||
var settings = Manager.ModuleSettings.GetSettings<AwakeSettings>(Constants.AppName);
|
||||
await WriteJsonResponseAsync(response, settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleErrorAsync(response, 500, "Settings module not available");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleErrorAsync(response, 500, $"Error retrieving settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleNotFoundAsync(HttpListenerResponse response, string path)
|
||||
{
|
||||
var errorResponse = new
|
||||
{
|
||||
Error = $"Awake endpoint '/{path}' not found",
|
||||
AvailableEndpoints = GetAvailableEndpoints(),
|
||||
Module = ModuleName,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
response.StatusCode = 404;
|
||||
await WriteJsonResponseAsync(response, errorResponse);
|
||||
}
|
||||
|
||||
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
|
||||
{
|
||||
response.StatusCode = statusCode;
|
||||
var errorResponse = new
|
||||
{
|
||||
Error = message,
|
||||
StatusCode = statusCode,
|
||||
Module = ModuleName,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
};
|
||||
await WriteJsonResponseAsync(response, errorResponse);
|
||||
}
|
||||
|
||||
private async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
|
||||
{
|
||||
// Use HttpServer utility method via static access or DI - for now, replicate the functionality
|
||||
response.ContentType = "application/json";
|
||||
response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(
|
||||
data, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
});
|
||||
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
|
||||
response.ContentLength64 = buffer.Length;
|
||||
await response.OutputStream.WriteAsync(buffer);
|
||||
response.Close();
|
||||
}
|
||||
|
||||
private async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
|
||||
where T : class
|
||||
{
|
||||
if (!request.HasEntityBody)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var reader = new System.IO.StreamReader(request.InputStream, System.Text.Encoding.UTF8);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(body, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
}
|
||||
catch (System.Text.Json.JsonException ex)
|
||||
{
|
||||
Logger.LogError($"Error deserializing request body: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Request DTOs
|
||||
private sealed class IndefiniteAwakeRequest
|
||||
{
|
||||
public bool? KeepDisplayOn { get; set; }
|
||||
|
||||
public int? ProcessId { get; set; }
|
||||
}
|
||||
|
||||
private sealed class TimedAwakeRequest
|
||||
{
|
||||
public uint Seconds { get; set; }
|
||||
|
||||
public bool? KeepDisplayOn { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ExpirableAwakeRequest
|
||||
{
|
||||
public DateTimeOffset ExpireAt { get; set; }
|
||||
|
||||
public bool? KeepDisplayOn { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ActivityBasedAwakeRequest
|
||||
{
|
||||
public uint CpuThresholdPercent { get; set; }
|
||||
|
||||
public uint MemThresholdPercent { get; set; }
|
||||
|
||||
public uint NetThresholdKBps { get; set; }
|
||||
|
||||
public uint SampleIntervalSeconds { get; set; }
|
||||
|
||||
public uint InactivityTimeoutSeconds { get; set; }
|
||||
|
||||
public bool? KeepDisplayOn { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -62,9 +63,23 @@ namespace Awake.Core
|
||||
// Foreground usage tracker instance (lifecycle managed by Program)
|
||||
internal static ForegroundUsageTracker? UsageTracker { get; set; }
|
||||
|
||||
private static PowerSchemeManager powerSchemeManager;
|
||||
|
||||
// Power scheme auto-switch (activity mode)
|
||||
private static string? _originalPowerSchemeGuid;
|
||||
private static bool _powerSchemeSwitched;
|
||||
|
||||
// Process monitoring fields
|
||||
private static List<string> _processMonitoringList = [];
|
||||
private static bool _processMonitoringActive;
|
||||
private static IDisposable? _processMonitoringSubscription;
|
||||
private static uint _processCheckInterval;
|
||||
private static bool _processKeepDisplay;
|
||||
|
||||
static Manager()
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
powerSchemeManager = new PowerSchemeManager();
|
||||
_stateQueue = [];
|
||||
ModuleSettings = new SettingsUtils();
|
||||
}
|
||||
@@ -129,6 +144,19 @@ namespace Awake.Core
|
||||
: ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
|
||||
}
|
||||
|
||||
// Activity mode state
|
||||
private static bool _activityActive;
|
||||
private static DateTimeOffset _activityLastHigh;
|
||||
private static uint _activityCpu;
|
||||
private static uint _activityMem;
|
||||
private static uint _activityNetKBps;
|
||||
private static uint _activitySample;
|
||||
private static uint _activityTimeout;
|
||||
private static bool _activityKeepDisplay;
|
||||
private static PerformanceCounter? _cpuCounter;
|
||||
private static PerformanceCounter? _memCounter;
|
||||
private static List<PerformanceCounter>? _netCounters;
|
||||
|
||||
internal static void CancelExistingThread()
|
||||
{
|
||||
Logger.LogInfo("Ensuring the thread is properly cleaned up...");
|
||||
@@ -136,6 +164,10 @@ namespace Awake.Core
|
||||
// Reset the thread state and handle cancellation.
|
||||
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
|
||||
|
||||
// Clean up process monitoring subscription if active
|
||||
_processMonitoringSubscription?.Dispose();
|
||||
_processMonitoringSubscription = null;
|
||||
|
||||
if (_tokenSource != null)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
@@ -180,6 +212,15 @@ namespace Awake.Core
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}]";
|
||||
icon = TrayHelper.TimedIcon;
|
||||
break;
|
||||
case AwakeMode.ACTIVITY:
|
||||
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_ACTIVITY}][{ScreenStateString}]";
|
||||
icon = TrayHelper.IndefiniteIcon; // Placeholder icon
|
||||
break;
|
||||
case AwakeMode.PROCESS:
|
||||
string processesText = _processMonitoringList.Count > 0 ? string.Join(", ", _processMonitoringList) : "None";
|
||||
iconText = $"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][{processesText}]";
|
||||
icon = TrayHelper.IndefiniteIcon; // Use same icon as indefinite for now
|
||||
break;
|
||||
}
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
@@ -189,6 +230,94 @@ namespace Awake.Core
|
||||
forceAdd ? TrayIconAction.Add : TrayIconAction.Update);
|
||||
}
|
||||
|
||||
private static void CaptureOriginalPowerScheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
powerSchemeManager.RefreshSchemes();
|
||||
_originalPowerSchemeGuid = powerSchemeManager
|
||||
.GetAllSchemes()
|
||||
.FirstOrDefault(s => s.IsActive)?.PSGuid;
|
||||
Logger.LogInfo($"Captured original power scheme: {_originalPowerSchemeGuid ?? "UNKNOWN"}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to capture original power scheme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SwitchToHighestPerformanceIfNeeded()
|
||||
{
|
||||
if (_powerSchemeSwitched)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
powerSchemeManager.RefreshSchemes();
|
||||
var highest = powerSchemeManager.GetHighestPerformanceScheme();
|
||||
if (highest == null)
|
||||
{
|
||||
Logger.LogInfo("No power schemes found when attempting high performance switch.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (highest.IsActive)
|
||||
{
|
||||
Logger.LogInfo("Already on highest performance scheme – no switch needed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_originalPowerSchemeGuid == null)
|
||||
{
|
||||
CaptureOriginalPowerScheme();
|
||||
}
|
||||
|
||||
if (powerSchemeManager.SwitchScheme(highest.PSGuid))
|
||||
{
|
||||
_powerSchemeSwitched = true;
|
||||
Logger.LogInfo($"Switched to highest performance scheme: {highest.Name} ({highest.PSGuid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Failed to switch to highest performance scheme: {highest.Name} ({highest.PSGuid})");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Exception while attempting to switch power scheme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreOriginalPowerSchemeIfNeeded()
|
||||
{
|
||||
if (!_powerSchemeSwitched || string.IsNullOrWhiteSpace(_originalPowerSchemeGuid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (powerSchemeManager.SwitchScheme(_originalPowerSchemeGuid))
|
||||
{
|
||||
Logger.LogInfo($"Restored original power scheme: {_originalPowerSchemeGuid}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Failed to restore original power scheme: {_originalPowerSchemeGuid}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Exception restoring original power scheme: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_powerSchemeSwitched = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false, int processId = 0, [CallerMemberName] string callerName = "")
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
|
||||
@@ -406,15 +535,11 @@ namespace Awake.Core
|
||||
internal static void CompleteExit(int exitCode)
|
||||
{
|
||||
SetPassiveKeepAwake(updateSettings: false);
|
||||
|
||||
RestoreOriginalPowerSchemeIfNeeded();
|
||||
if (TrayHelper.WindowHandle != IntPtr.Zero)
|
||||
{
|
||||
// Delete the icon.
|
||||
TrayHelper.SetShellIcon(TrayHelper.WindowHandle, string.Empty, null, TrayIconAction.Delete);
|
||||
|
||||
// Close the message window that we used for the tray.
|
||||
Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0);
|
||||
|
||||
Bridge.DestroyWindow(TrayHelper.WindowHandle);
|
||||
}
|
||||
|
||||
@@ -516,6 +641,275 @@ namespace Awake.Core
|
||||
SetModeShellIcon();
|
||||
}
|
||||
|
||||
internal static void SetActivityBasedKeepAwake(
|
||||
uint cpuThresholdPercent,
|
||||
uint memThresholdPercent,
|
||||
uint netThresholdKBps,
|
||||
uint sampleIntervalSeconds,
|
||||
uint inactivityTimeoutSeconds,
|
||||
bool keepDisplayOn,
|
||||
[CallerMemberName] string callerName = "")
|
||||
{
|
||||
Logger.LogInfo($"Activity-based keep-awake invoked by {callerName}. CPU>={cpuThresholdPercent}%, MEM>={memThresholdPercent}%, NET>={netThresholdKBps}KB/s sample={sampleIntervalSeconds}s timeout={inactivityTimeoutSeconds}s display={keepDisplayOn}.");
|
||||
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
bool settingsChanged =
|
||||
currentSettings.Properties.Mode != AwakeMode.ACTIVITY ||
|
||||
currentSettings.Properties.ActivityCpuThresholdPercent != cpuThresholdPercent ||
|
||||
currentSettings.Properties.ActivityMemoryThresholdPercent != memThresholdPercent ||
|
||||
currentSettings.Properties.ActivityNetworkThresholdKBps != netThresholdKBps ||
|
||||
currentSettings.Properties.ActivitySampleIntervalSeconds != sampleIntervalSeconds ||
|
||||
currentSettings.Properties.ActivityInactivityTimeoutSeconds != inactivityTimeoutSeconds ||
|
||||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
|
||||
|
||||
if (settingsChanged)
|
||||
{
|
||||
currentSettings.Properties.Mode = AwakeMode.ACTIVITY;
|
||||
currentSettings.Properties.ActivityCpuThresholdPercent = cpuThresholdPercent;
|
||||
currentSettings.Properties.ActivityMemoryThresholdPercent = memThresholdPercent;
|
||||
currentSettings.Properties.ActivityNetworkThresholdKBps = netThresholdKBps;
|
||||
currentSettings.Properties.ActivitySampleIntervalSeconds = sampleIntervalSeconds;
|
||||
currentSettings.Properties.ActivityInactivityTimeoutSeconds = inactivityTimeoutSeconds;
|
||||
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
|
||||
return; // Settings will be processed triggering this again
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to persist activity mode settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize performance counters
|
||||
try
|
||||
{
|
||||
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
||||
_memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use");
|
||||
_netCounters = PerformanceCounterCategory.GetCategories()
|
||||
.FirstOrDefault(c => c.CategoryName == "Network Interface")?
|
||||
.GetInstanceNames()
|
||||
.Select(n => new PerformanceCounter("Network Interface", "Bytes Total/sec", n))
|
||||
.ToList() ?? new List<PerformanceCounter>();
|
||||
_cpuCounter.NextValue(); // Prime CPU counter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to initialize performance counters for activity mode: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
_activityCpu = cpuThresholdPercent;
|
||||
_activityMem = memThresholdPercent;
|
||||
_activityNetKBps = netThresholdKBps;
|
||||
_activitySample = Math.Max(1, sampleIntervalSeconds);
|
||||
_activityTimeout = Math.Max(5, inactivityTimeoutSeconds);
|
||||
_activityKeepDisplay = keepDisplayOn;
|
||||
_activityLastHigh = DateTimeOffset.Now;
|
||||
_activityActive = true;
|
||||
|
||||
CurrentOperatingMode = AwakeMode.ACTIVITY;
|
||||
IsDisplayOn = keepDisplayOn;
|
||||
SetModeShellIcon();
|
||||
|
||||
// Capture original scheme before any switch
|
||||
CaptureOriginalPowerScheme();
|
||||
|
||||
TimeSpan sampleInterval = TimeSpan.FromSeconds(_activitySample);
|
||||
|
||||
Observable.Interval(sampleInterval).Subscribe(
|
||||
_ =>
|
||||
{
|
||||
if (!_activityActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float cpu = 0;
|
||||
float mem = 0;
|
||||
double netKBps = 0;
|
||||
|
||||
try
|
||||
{
|
||||
cpu = _cpuCounter?.NextValue() ?? 0;
|
||||
mem = _memCounter?.NextValue() ?? 0;
|
||||
if (_netCounters != null && _netCounters.Count > 0)
|
||||
{
|
||||
netKBps = _netCounters.Sum(c => c.NextValue()) / 1024.0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Performance counter read failure: {ex.Message}");
|
||||
}
|
||||
|
||||
bool above =
|
||||
(_activityCpu == 0 || cpu >= _activityCpu) ||
|
||||
(_activityMem == 0 || mem >= _activityMem) ||
|
||||
(_activityNetKBps == 0 || netKBps >= _activityNetKBps);
|
||||
|
||||
if (above)
|
||||
{
|
||||
_activityLastHigh = DateTimeOffset.Now;
|
||||
_stateQueue.Add(ComputeAwakeState(_activityKeepDisplay));
|
||||
SwitchToHighestPerformanceIfNeeded();
|
||||
}
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName} [Activity][{ScreenStateString}][CPU {cpu:0.#}% | MEM {mem:0.#}% | NET {netKBps:0.#}KB/s]",
|
||||
TrayHelper.IndefiniteIcon,
|
||||
TrayIconAction.Update);
|
||||
|
||||
if ((DateTimeOffset.Now - _activityLastHigh).TotalSeconds >= _activityTimeout)
|
||||
{
|
||||
Logger.LogInfo("Activity thresholds not met within timeout window. Ending activity mode.");
|
||||
_activityActive = false;
|
||||
CancelExistingThread();
|
||||
RestoreOriginalPowerSchemeIfNeeded();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
}
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
Logger.LogError($"Activity mode observable failure: {ex.Message}");
|
||||
},
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
internal static void SetProcessBasedKeepAwake(
|
||||
List<string> processNames,
|
||||
uint checkIntervalSeconds,
|
||||
bool keepDisplayOn,
|
||||
[CallerMemberName] string callerName = "")
|
||||
{
|
||||
Logger.LogInfo($"Process-based keep-awake invoked by {callerName}. Processes: [{string.Join(", ", processNames)}], CheckInterval: {checkIntervalSeconds}s, Display: {keepDisplayOn}.");
|
||||
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
bool settingsChanged =
|
||||
currentSettings.Properties.Mode != AwakeMode.PROCESS ||
|
||||
!currentSettings.Properties.ProcessMonitoringList.SequenceEqual(processNames) ||
|
||||
currentSettings.Properties.ProcessCheckIntervalSeconds != checkIntervalSeconds ||
|
||||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
|
||||
|
||||
if (settingsChanged)
|
||||
{
|
||||
currentSettings.Properties.Mode = AwakeMode.PROCESS;
|
||||
currentSettings.Properties.ProcessMonitoringList = new List<string>(processNames);
|
||||
currentSettings.Properties.ProcessCheckIntervalSeconds = checkIntervalSeconds;
|
||||
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
|
||||
return; // Settings will be processed triggering this again
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to persist process monitoring settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_processMonitoringList = [.. processNames];
|
||||
_processCheckInterval = Math.Max(1, checkIntervalSeconds);
|
||||
_processKeepDisplay = keepDisplayOn;
|
||||
_processMonitoringActive = true;
|
||||
|
||||
CurrentOperatingMode = AwakeMode.PROCESS;
|
||||
IsDisplayOn = keepDisplayOn;
|
||||
SetModeShellIcon();
|
||||
|
||||
TimeSpan checkInterval = TimeSpan.FromSeconds(_processCheckInterval);
|
||||
|
||||
Observable.Interval(checkInterval).Subscribe(
|
||||
_ =>
|
||||
{
|
||||
if (!_processMonitoringActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool anyTargetProcessRunning = false;
|
||||
List<string> runningProcesses = new();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string processName in _processMonitoringList)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(processName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove .exe extension if present for process name comparison
|
||||
string processNameWithoutExt = processName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
|
||||
? processName.Substring(0, processName.Length - 4)
|
||||
: processName;
|
||||
|
||||
Process[] processes = Process.GetProcessesByName(processNameWithoutExt);
|
||||
if (processes.Length > 0)
|
||||
{
|
||||
anyTargetProcessRunning = true;
|
||||
runningProcesses.Add(processNameWithoutExt);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Process monitoring check failure: {ex.Message}");
|
||||
}
|
||||
|
||||
if (anyTargetProcessRunning)
|
||||
{
|
||||
_stateQueue.Add(ComputeAwakeState(_processKeepDisplay));
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][Running: {string.Join(", ", runningProcesses)}]",
|
||||
TrayHelper.IndefiniteIcon,
|
||||
TrayIconAction.Update);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("No target processes running. Ending process monitoring mode.");
|
||||
_processMonitoringActive = false;
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
}
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
Logger.LogError($"Process monitoring observable failure: {ex.Message}");
|
||||
},
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the display settings.
|
||||
/// </summary>
|
||||
@@ -549,6 +943,100 @@ namespace Awake.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Awake configuration status.
|
||||
/// </summary>
|
||||
/// <returns>Current configuration status information.</returns>
|
||||
public static object GetCurrentConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
AwakeSettings currentSettings = ModuleSettings?.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
|
||||
|
||||
var baseConfig = new
|
||||
{
|
||||
mode = CurrentOperatingMode.ToString(),
|
||||
keepDisplayOn = IsDisplayOn,
|
||||
processId = ProcessId,
|
||||
isUsingPowerToysConfig = IsUsingPowerToysConfig,
|
||||
};
|
||||
|
||||
// Return different parameters based on the current mode
|
||||
return CurrentOperatingMode switch
|
||||
{
|
||||
AwakeMode.PASSIVE => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
AwakeMode.INDEFINITE => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
AwakeMode.TIMED => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
timeRemaining = TimeRemaining,
|
||||
intervalHours = currentSettings.Properties.IntervalHours,
|
||||
intervalMinutes = currentSettings.Properties.IntervalMinutes,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
AwakeMode.EXPIRABLE => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
expireAt = ExpireAt == DateTimeOffset.MinValue ? (DateTimeOffset?)null : ExpireAt,
|
||||
expirationDateTime = currentSettings.Properties.ExpirationDateTime,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
AwakeMode.ACTIVITY => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
cpuThresholdPercent = currentSettings.Properties.ActivityCpuThresholdPercent,
|
||||
memoryThresholdPercent = currentSettings.Properties.ActivityMemoryThresholdPercent,
|
||||
networkThresholdKBps = currentSettings.Properties.ActivityNetworkThresholdKBps,
|
||||
sampleIntervalSeconds = currentSettings.Properties.ActivitySampleIntervalSeconds,
|
||||
inactivityTimeoutSeconds = currentSettings.Properties.ActivityInactivityTimeoutSeconds,
|
||||
activityActive = _activityActive,
|
||||
lastHighActivity = _activityLastHigh,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
AwakeMode.PROCESS => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
processMonitoringList = currentSettings.Properties.ProcessMonitoringList,
|
||||
processCheckIntervalSeconds = currentSettings.Properties.ProcessCheckIntervalSeconds,
|
||||
processMonitoringActive = _processMonitoringActive,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
},
|
||||
_ => new
|
||||
{
|
||||
baseConfig.mode,
|
||||
baseConfig.keepDisplayOn,
|
||||
baseConfig.processId,
|
||||
baseConfig.isUsingPowerToysConfig,
|
||||
error = "Unknown mode",
|
||||
},
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to get current config: {ex.Message}");
|
||||
return new { error = "Failed to get current configuration", message = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
public static Process? GetParentProcess()
|
||||
{
|
||||
return GetParentProcess(Process.GetCurrentProcess().Handle);
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Awake.Core.Models
|
||||
TC_MODE_PASSIVE = Native.Constants.WM_USER + 0x3,
|
||||
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 0x4,
|
||||
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 0x5,
|
||||
TC_MODE_ACTIVITY = Native.Constants.WM_USER + 0x6,
|
||||
TC_EXIT = Native.Constants.WM_USER + 0x64,
|
||||
TC_TIME = Native.Constants.WM_USER + 0x65,
|
||||
}
|
||||
|
||||
136
src/modules/awake/Awake/Core/PowerSchemeManager.cs
Normal file
136
src/modules/awake/Awake/Core/PowerSchemeManager.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
|
||||
namespace Awake.Core
|
||||
{
|
||||
public class PowerSchemeManager
|
||||
{
|
||||
private readonly List<PowerScheme> _schemes = new();
|
||||
|
||||
public PowerSchemeManager()
|
||||
{
|
||||
RefreshSchemes();
|
||||
}
|
||||
|
||||
public void RefreshSchemes()
|
||||
{
|
||||
_schemes.Clear();
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powercfg",
|
||||
Arguments = "/L",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
};
|
||||
process.Start();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
var matches = Regex.Matches(output, @"Power Scheme GUID: ([a-fA-F0-9\-]+)\s+\((.+?)\)(\s+\*)?");
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
_schemes.Add(new PowerScheme
|
||||
{
|
||||
PSGuid = match.Groups[1].Value,
|
||||
Name = match.Groups[2].Value,
|
||||
IsActive = match.Groups[3].Value.Contains('*'),
|
||||
});
|
||||
}
|
||||
|
||||
// Rank schemes by performance (descending)
|
||||
_schemes.Sort((a, b) => GetScore(b).CompareTo(GetScore(a)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all power schemes sorted by performance (highest first).
|
||||
/// </summary>
|
||||
public IReadOnlyList<PowerScheme> GetAllSchemes() => _schemes;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the highest performance scheme currently available (may already be active).
|
||||
/// </summary>
|
||||
public PowerScheme? GetHighestPerformanceScheme()
|
||||
{
|
||||
if (_schemes.Count == 0)
|
||||
{
|
||||
RefreshSchemes();
|
||||
}
|
||||
|
||||
return _schemes.Count == 0 ? null : _schemes[0];
|
||||
}
|
||||
|
||||
public bool SwitchScheme(string psGuid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powercfg",
|
||||
Arguments = $"/setactive {psGuid}",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
RefreshSchemes();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetScore(PowerScheme scheme)
|
||||
{
|
||||
// Heuristic based on name (multi-language basic keywords).
|
||||
string name = scheme.Name.ToLowerInvariant();
|
||||
|
||||
// High performance indicators
|
||||
if (name.Contains("ultimate") || name.Contains("ultra"))
|
||||
{
|
||||
return 380;
|
||||
}
|
||||
|
||||
if (name.Contains("high"))
|
||||
{
|
||||
return 310;
|
||||
}
|
||||
|
||||
if (name.Contains("balanced"))
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
if (name.Contains("saver"))
|
||||
{
|
||||
return 120;
|
||||
}
|
||||
|
||||
// Default for unknown custom plans.
|
||||
return 180;
|
||||
}
|
||||
|
||||
public class PowerScheme
|
||||
{
|
||||
public string PSGuid { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,19 @@ namespace Awake.Core
|
||||
break;
|
||||
}
|
||||
|
||||
case (uint)TrayCommands.TC_MODE_ACTIVITY:
|
||||
{
|
||||
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
|
||||
Manager.SetActivityBasedKeepAwake(
|
||||
settings.Properties.ActivityCpuThresholdPercent,
|
||||
settings.Properties.ActivityMemoryThresholdPercent,
|
||||
settings.Properties.ActivityNetworkThresholdKBps,
|
||||
settings.Properties.ActivitySampleIntervalSeconds,
|
||||
settings.Properties.ActivityInactivityTimeoutSeconds,
|
||||
settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
}
|
||||
|
||||
case (uint)TrayCommands.TC_MODE_PASSIVE:
|
||||
{
|
||||
Manager.SetPassiveKeepAwake();
|
||||
@@ -455,6 +468,7 @@ namespace Awake.Core
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF, mode == AwakeMode.PASSIVE);
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY, mode == AwakeMode.INDEFINITE);
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION, mode == AwakeMode.EXPIRABLE, true);
|
||||
InsertMenuItem(0, TrayCommands.TC_MODE_ACTIVITY, Resources.AWAKE_KEEP_ON_ACTIVITY, mode == AwakeMode.ACTIVITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
src/modules/awake/Awake/IAwakeAutomation.cs
Normal file
31
src/modules/awake/Awake/IAwakeAutomation.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,22 @@ namespace Awake
|
||||
private static readonly string[] _aliasesPidOption = ["--pid", "-p"];
|
||||
private static readonly string[] _aliasesExpireAtOption = ["--expire-at", "-e"];
|
||||
private static readonly string[] _aliasesParentPidOption = ["--use-parent-pid", "-u"];
|
||||
private static readonly string[] _aliasesHttpServerOption = ["--http-server", "-s"];
|
||||
private static readonly string[] _aliasesHttpPortOption = ["--http-port"];
|
||||
private static readonly string[] _aliasesActivityOption = ["--activity", "-a"];
|
||||
private static readonly string[] _aliasesActivityCpuOption = ["--act-cpu"];
|
||||
private static readonly string[] _aliasesActivityMemOption = ["--act-mem"];
|
||||
private static readonly string[] _aliasesActivityNetOption = ["--act-net"];
|
||||
private static readonly string[] _aliasesActivitySampleOption = ["--act-sample"];
|
||||
private static readonly string[] _aliasesActivityTimeoutOption = ["--act-timeout"];
|
||||
|
||||
private static readonly JsonSerializerOptions _serializerOptions = new() { IncludeFields = true };
|
||||
private static readonly ETWTrace _etwTrace = new();
|
||||
|
||||
private static FileSystemWatcher? _watcher;
|
||||
private static SettingsUtils? _settingsUtils;
|
||||
private static ManagedCommon.HttpServer? _httpServer;
|
||||
private static AwakeHttpHandler? _awakeHttpHandler;
|
||||
|
||||
private static bool _startedFromPowerToys;
|
||||
|
||||
@@ -74,7 +84,11 @@ namespace Awake
|
||||
}
|
||||
|
||||
await TrayHelper.InitializeTray(TrayHelper.DefaultAwakeIcon, Core.Constants.FullAppName);
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() => LockMutex?.ReleaseMutex());
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() =>
|
||||
{
|
||||
_httpServer?.Dispose();
|
||||
LockMutex?.ReleaseMutex();
|
||||
});
|
||||
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
|
||||
|
||||
if (!instantiated)
|
||||
@@ -99,6 +113,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!
|
||||
@@ -148,6 +168,50 @@ namespace Awake
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
Option<bool> httpServerOption = new(_aliasesHttpServerOption, () => true, "Enable HTTP server for remote control")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
Option<int> httpPortOption = new(_aliasesHttpPortOption, () => 8080, "HTTP server port (default: 8080)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
// Activity-based keep-awake options
|
||||
Option<bool> activityOption = new(_aliasesActivityOption, () => false, "Enable activity-based keep awake mode (CPU / Memory / Network)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
Option<uint> activityCpuOption = new(_aliasesActivityCpuOption, () => 20, "Activity mode CPU threshold percent (0-100, 0 to ignore)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
Option<uint> activityMemOption = new(_aliasesActivityMemOption, () => 50, "Activity mode Memory usage threshold percent (0-100, 0 to ignore)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
Option<uint> activityNetOption = new(_aliasesActivityNetOption, () => 100, "Activity mode total network throughput threshold KB/s (0 to ignore)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
Option<uint> activitySampleOption = new(_aliasesActivitySampleOption, () => 5, "Activity mode sampling interval in seconds (min 1)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
Option<uint> activityTimeoutOption = new(_aliasesActivityTimeoutOption, () => 60, "Activity mode inactivity timeout in seconds before reverting (min 5)")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
timeOption.AddValidator(result =>
|
||||
{
|
||||
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
|
||||
@@ -178,6 +242,16 @@ namespace Awake
|
||||
}
|
||||
});
|
||||
|
||||
httpPortOption.AddValidator(result =>
|
||||
{
|
||||
if (result.Tokens.Count != 0 && (!int.TryParse(result.Tokens[0].Value, out int port) || port < 1 || port > 65535))
|
||||
{
|
||||
string errorMessage = $"HTTP port value could not be parsed correctly or is out of range (1-65535). Value used: {result.Tokens[0].Value}.";
|
||||
Logger.LogError(errorMessage);
|
||||
result.ErrorMessage = errorMessage;
|
||||
}
|
||||
});
|
||||
|
||||
RootCommand? rootCommand =
|
||||
[
|
||||
configOption,
|
||||
@@ -186,10 +260,56 @@ namespace Awake
|
||||
pidOption,
|
||||
expireAtOption,
|
||||
parentPidOption,
|
||||
httpServerOption,
|
||||
httpPortOption,
|
||||
activityOption,
|
||||
activityCpuOption,
|
||||
activityMemOption,
|
||||
activityNetOption,
|
||||
activitySampleOption,
|
||||
activityTimeoutOption,
|
||||
];
|
||||
|
||||
rootCommand.Description = Core.Constants.AppName;
|
||||
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
|
||||
|
||||
// NOTE: The strongly-typed SetHandler overloads have an upper limit on the number of symbols
|
||||
// they accept. We exceed that limit with the new activity-mode parameters. Switch to a context
|
||||
// based handler and manually extract option values from the ParseResult.
|
||||
rootCommand.SetHandler(context =>
|
||||
{
|
||||
var pr = context.ParseResult;
|
||||
|
||||
bool usePtConfig = pr.GetValueForOption(configOption);
|
||||
bool displayOn = pr.GetValueForOption(displayOption);
|
||||
uint timeLimit = pr.GetValueForOption(timeOption);
|
||||
int pid = pr.GetValueForOption(pidOption);
|
||||
string expireAt = pr.GetValueForOption(expireAtOption) ?? string.Empty;
|
||||
bool useParentPid = pr.GetValueForOption(parentPidOption);
|
||||
bool enableHttpServer = pr.GetValueForOption(httpServerOption);
|
||||
int httpPort = pr.GetValueForOption(httpPortOption);
|
||||
bool activityMode = pr.GetValueForOption(activityOption);
|
||||
uint actCpu = pr.GetValueForOption(activityCpuOption);
|
||||
uint actMem = pr.GetValueForOption(activityMemOption);
|
||||
uint actNet = pr.GetValueForOption(activityNetOption);
|
||||
uint actSample = pr.GetValueForOption(activitySampleOption);
|
||||
uint actTimeout = pr.GetValueForOption(activityTimeoutOption);
|
||||
|
||||
HandleCommandLineArguments(
|
||||
usePtConfig,
|
||||
displayOn,
|
||||
timeLimit,
|
||||
pid,
|
||||
expireAt,
|
||||
useParentPid,
|
||||
enableHttpServer,
|
||||
httpPort,
|
||||
activityMode,
|
||||
actCpu,
|
||||
actMem,
|
||||
actNet,
|
||||
actSample,
|
||||
actTimeout);
|
||||
});
|
||||
|
||||
return rootCommand.InvokeAsync(args).Result;
|
||||
}
|
||||
@@ -214,12 +334,13 @@ namespace Awake
|
||||
|
||||
private static void Exit(string message, int exitCode)
|
||||
{
|
||||
_httpServer?.Dispose();
|
||||
_etwTrace?.Dispose();
|
||||
Logger.LogInfo(message);
|
||||
Manager.CompleteExit(exitCode);
|
||||
}
|
||||
|
||||
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid)
|
||||
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid, bool enableHttpServer, int httpPort, bool activityMode, uint actCpu, uint actMem, uint actNet, uint actSample, uint actTimeout)
|
||||
{
|
||||
if (pid == 0 && !useParentPid)
|
||||
{
|
||||
@@ -238,6 +359,35 @@ namespace Awake
|
||||
Logger.LogInfo($"The value for --pid is: {pid}");
|
||||
Logger.LogInfo($"The value for --expire-at is: {expireAt}");
|
||||
Logger.LogInfo($"The value for --use-parent-pid is: {useParentPid}");
|
||||
Logger.LogInfo($"The value for --http-server is: {enableHttpServer}");
|
||||
Logger.LogInfo($"The value for --http-port is: {httpPort}");
|
||||
Logger.LogInfo($"The value for --activity is: {activityMode}");
|
||||
if (activityMode)
|
||||
{
|
||||
Logger.LogInfo($"Activity thresholds CPU={actCpu}% MEM={actMem}% NET={actNet}KB/s SAMPLE={actSample}s TIMEOUT={actTimeout}s");
|
||||
}
|
||||
|
||||
// Initialize HTTP server if requested
|
||||
if (enableHttpServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
string prefix = $"http://localhost:{httpPort}/";
|
||||
_httpServer = new ManagedCommon.HttpServer(prefix);
|
||||
|
||||
// Register Awake-specific handler
|
||||
_awakeHttpHandler = new AwakeHttpHandler();
|
||||
_httpServer.RegisterHandler(_awakeHttpHandler);
|
||||
|
||||
_httpServer.Start();
|
||||
Logger.LogInfo($"HTTP server started on {prefix}");
|
||||
Logger.LogInfo($"Available endpoints: GET {prefix}status, GET {prefix}awake/...");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Start the monitor thread that will be used to track the current state.
|
||||
Manager.StartMonitor();
|
||||
@@ -288,6 +438,11 @@ namespace Awake
|
||||
Logger.LogError($"There was a problem with the configuration file. Make sure it exists. {ex.Message}");
|
||||
}
|
||||
}
|
||||
else if (!usePtConfig && activityMode)
|
||||
{
|
||||
Logger.LogInfo("Starting activity-based keep-awake mode (CLI override).");
|
||||
Manager.SetActivityBasedKeepAwake(actCpu, actMem, actNet, actSample, actTimeout, displayOn);
|
||||
}
|
||||
else if (pid != 0 || useParentPid)
|
||||
{
|
||||
// Second, we snap to process-based execution. Because this is something that
|
||||
@@ -480,6 +635,22 @@ namespace Awake
|
||||
|
||||
Manager.SetExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
case AwakeMode.ACTIVITY:
|
||||
Manager.SetActivityBasedKeepAwake(
|
||||
settings.Properties.ActivityCpuThresholdPercent,
|
||||
settings.Properties.ActivityMemoryThresholdPercent,
|
||||
settings.Properties.ActivityNetworkThresholdKBps,
|
||||
settings.Properties.ActivitySampleIntervalSeconds,
|
||||
settings.Properties.ActivityInactivityTimeoutSeconds,
|
||||
settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
|
||||
case AwakeMode.PROCESS:
|
||||
Manager.SetProcessBasedKeepAwake(
|
||||
settings.Properties.ProcessMonitoringList,
|
||||
settings.Properties.ProcessCheckIntervalSeconds,
|
||||
settings.Properties.KeepDisplayOn);
|
||||
break;
|
||||
|
||||
default:
|
||||
Logger.LogError("Unknown mode of operation. Check config file.");
|
||||
|
||||
@@ -213,6 +213,15 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Keep awake on activity (CPU / Memory / Network).
|
||||
/// </summary>
|
||||
internal static string AWAKE_KEEP_ON_ACTIVITY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_KEEP_ON_ACTIVITY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to d.
|
||||
/// </summary>
|
||||
@@ -339,6 +348,15 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Activity.
|
||||
/// </summary>
|
||||
internal static string AWAKE_TRAY_TEXT_ACTIVITY {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_TRAY_TEXT_ACTIVITY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unchecked.
|
||||
/// </summary>
|
||||
|
||||
@@ -146,6 +146,10 @@
|
||||
<value>Keep awake until expiration date and time</value>
|
||||
<comment>Keep the system awake until expiration date and time</comment>
|
||||
</data>
|
||||
<data name="AWAKE_KEEP_ON_ACTIVITY" xml:space="preserve">
|
||||
<value>Keep awake on activity (CPU / Memory / Network)</value>
|
||||
<comment>Keep the system awake while resource activity is above configured thresholds</comment>
|
||||
</data>
|
||||
<data name="AWAKE_MINUTE" xml:space="preserve">
|
||||
<value>{0} minute</value>
|
||||
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
|
||||
@@ -197,6 +201,9 @@
|
||||
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
|
||||
<value>Interval</value>
|
||||
</data>
|
||||
<data name="AWAKE_TRAY_TEXT_ACTIVITY" xml:space="preserve">
|
||||
<value>Activity</value>
|
||||
</data>
|
||||
<data name="AWAKE_LABEL_DAYS" xml:space="preserve">
|
||||
<value>d</value>
|
||||
<comment>Used to display number of days in the system tray tooltip.</comment>
|
||||
|
||||
131
src/modules/awake/scripts/AwakeRotMini.ps1
Normal file
131
src/modules/awake/scripts/AwakeRotMini.ps1
Normal 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 dependency‑light 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 }
|
||||
}
|
||||
187
src/modules/awake/scripts/AwakeRotTest.ps1
Normal file
187
src/modules/awake/scripts/AwakeRotTest.ps1
Normal file
@@ -0,0 +1,187 @@
|
||||
<#
|
||||
Minimal ROT test script for the runtime‑registered 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 self‑contained; 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) }
|
||||
}
|
||||
@@ -10,5 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
INDEFINITE = 1,
|
||||
TIMED = 2,
|
||||
EXPIRABLE = 3,
|
||||
ACTIVITY = 4, // Resource activity based keep-awake
|
||||
PROCESS = 5, // Process-based keep-awake
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
ExpirationDateTime = DateTimeOffset.Now;
|
||||
CustomTrayTimes = [];
|
||||
|
||||
// Defaults for activity-based mode
|
||||
ActivityCpuThresholdPercent = 20;
|
||||
ActivityMemoryThresholdPercent = 50;
|
||||
ActivityNetworkThresholdKBps = 100;
|
||||
ActivitySampleIntervalSeconds = 5;
|
||||
ActivityInactivityTimeoutSeconds = 60;
|
||||
|
||||
// Usage tracking defaults (opt-in, disabled by default)
|
||||
TrackUsageEnabled = false; // default off
|
||||
UsageRetentionDays = 14; // two weeks default retention
|
||||
|
||||
// Process monitoring defaults
|
||||
ProcessMonitoringList = new List<string>();
|
||||
ProcessCheckIntervalSeconds = 5; // Check every 5 seconds
|
||||
}
|
||||
|
||||
[JsonPropertyName("keepDisplayOn")]
|
||||
@@ -45,6 +56,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[CmdConfigureIgnore]
|
||||
public Dictionary<string, uint> CustomTrayTimes { get; set; }
|
||||
|
||||
// Activity-based mode configuration
|
||||
[JsonPropertyName("activityCpuThresholdPercent")]
|
||||
public uint ActivityCpuThresholdPercent { get; set; }
|
||||
|
||||
[JsonPropertyName("activityMemoryThresholdPercent")]
|
||||
public uint ActivityMemoryThresholdPercent { get; set; }
|
||||
|
||||
[JsonPropertyName("activityNetworkThresholdKBps")]
|
||||
public uint ActivityNetworkThresholdKBps { get; set; }
|
||||
|
||||
[JsonPropertyName("activitySampleIntervalSeconds")]
|
||||
public uint ActivitySampleIntervalSeconds { get; set; }
|
||||
|
||||
[JsonPropertyName("activityInactivityTimeoutSeconds")]
|
||||
public uint ActivityInactivityTimeoutSeconds { get; set; }
|
||||
|
||||
// New opt-in usage tracking flag
|
||||
[JsonPropertyName("trackUsageEnabled")]
|
||||
public bool TrackUsageEnabled { get; set; }
|
||||
@@ -52,5 +79,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// Retention window for usage data (days)
|
||||
[JsonPropertyName("usageRetentionDays")]
|
||||
public int UsageRetentionDays { get; set; }
|
||||
|
||||
// Process monitoring configuration
|
||||
[JsonPropertyName("processMonitoringList")]
|
||||
public List<string> ProcessMonitoringList { get; set; }
|
||||
|
||||
[JsonPropertyName("processCheckIntervalSeconds")]
|
||||
public uint ProcessCheckIntervalSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -38,9 +39,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
KeepDisplayOn = Properties.KeepDisplayOn,
|
||||
IntervalMinutes = Properties.IntervalMinutes,
|
||||
IntervalHours = Properties.IntervalHours,
|
||||
ActivityCpuThresholdPercent = Properties.ActivityCpuThresholdPercent,
|
||||
ActivityMemoryThresholdPercent = Properties.ActivityMemoryThresholdPercent,
|
||||
ActivityNetworkThresholdKBps = Properties.ActivityNetworkThresholdKBps,
|
||||
ActivitySampleIntervalSeconds = Properties.ActivitySampleIntervalSeconds,
|
||||
ActivityInactivityTimeoutSeconds = Properties.ActivityInactivityTimeoutSeconds,
|
||||
|
||||
// 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,
|
||||
|
||||
// Process monitoring properties
|
||||
ProcessMonitoringList = Properties.ProcessMonitoringList != null ? new List<string>(Properties.ProcessMonitoringList) : new List<string>(),
|
||||
ProcessCheckIntervalSeconds = Properties.ProcessCheckIntervalSeconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
<ComboBoxItem x:Uid="Awake_IndefiniteKeepAwakeSelector" />
|
||||
<ComboBoxItem x:Uid="Awake_TemporaryKeepAwakeSelector" />
|
||||
<ComboBoxItem x:Uid="Awake_ExpirableKeepAwakeSelector" />
|
||||
<ComboBoxItem x:Uid="Awake_ActivityKeepAwakeSelector" />
|
||||
<ComboBoxItem x:Uid="Awake_ProcessKeepAwakeSelector" />
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
@@ -100,6 +102,105 @@
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<!-- Activity mode configuration -->
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AwakeActivitySettingsExpander"
|
||||
x:Uid="Awake_ActivitySettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True"
|
||||
Visibility="{x:Bind ViewModel.IsActivityConfigurationEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard Name="AwakeActivityCpuThresholdCard" x:Uid="Awake_ActivityCpuThresholdCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ActivityCpuThresholdInput"
|
||||
Width="120"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ActivityCpuThresholdPercent, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AwakeActivityMemoryThresholdCard" x:Uid="Awake_ActivityMemoryThresholdCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ActivityMemoryThresholdInput"
|
||||
Width="120"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ActivityMemoryThresholdPercent, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AwakeActivityNetworkThresholdCard" x:Uid="Awake_ActivityNetworkThresholdCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ActivityNetworkThresholdInput"
|
||||
Width="120"
|
||||
Minimum="0"
|
||||
SmallChange="10"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ActivityNetworkThresholdKBps, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AwakeActivitySampleIntervalCard" x:Uid="Awake_ActivitySampleIntervalCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ActivitySampleIntervalInput"
|
||||
Width="120"
|
||||
Minimum="1"
|
||||
Maximum="3600"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ActivitySampleIntervalSeconds, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AwakeActivityInactivityTimeoutCard" x:Uid="Awake_ActivityInactivityTimeoutCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ActivityInactivityTimeoutInput"
|
||||
Width="120"
|
||||
Minimum="5"
|
||||
Maximum="86400"
|
||||
SmallChange="5"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ActivityInactivityTimeoutSeconds, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<!-- Process mode configuration -->
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AwakeProcessSettingsExpander"
|
||||
x:Uid="Awake_ProcessSettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True"
|
||||
Visibility="{x:Bind ViewModel.IsProcessConfigurationEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard Name="AwakeProcessListCard" x:Uid="Awake_ProcessListCard">
|
||||
<TextBox
|
||||
x:Uid="Awake_ProcessListInput"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Text="{x:Bind ViewModel.ProcessMonitoringList, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AwakeProcessCheckIntervalCard" x:Uid="Awake_ProcessCheckIntervalCard">
|
||||
<StackPanel Orientation="Horizontal" MinWidth="{StaticResource SettingActionControlMinWidth}">
|
||||
<NumberBox
|
||||
x:Uid="Awake_ProcessCheckIntervalInput"
|
||||
Width="120"
|
||||
Minimum="1"
|
||||
Maximum="300"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.ProcessCheckIntervalSeconds, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<!-- Usage tracking settings -->
|
||||
<tkcontrols:SettingsCard
|
||||
Name="AwakeUsageTrackingEnabledCard"
|
||||
|
||||
@@ -2357,6 +2357,12 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="Awake_ExpirableKeepAwakeSelector.Content" xml:space="preserve">
|
||||
<value>Keep awake until expiration</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityKeepAwakeSelector.Content" xml:space="preserve">
|
||||
<value>Keep awake while system is active</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessKeepAwakeSelector.Content" xml:space="preserve">
|
||||
<value>Keep awake while process is running</value>
|
||||
</data>
|
||||
<data name="Awake_DisplaySettingsCard.Header" xml:space="preserve">
|
||||
<value>Keep screen on</value>
|
||||
</data>
|
||||
@@ -2384,6 +2390,63 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="Awake_ExpirationSettingsExpander_Time.Header" xml:space="preserve">
|
||||
<value>End time</value>
|
||||
</data>
|
||||
<data name="Awake_ActivitySettingsExpander.Header" xml:space="preserve">
|
||||
<value>Activity thresholds</value>
|
||||
</data>
|
||||
<data name="Awake_ActivitySettingsExpander.Description" xml:space="preserve">
|
||||
<value>Stay awake while any enabled metric exceeds its threshold</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityCpuThresholdCard.Header" xml:space="preserve">
|
||||
<value>CPU usage (%)</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityCpuThresholdInput.Header" xml:space="preserve">
|
||||
<value>CPU %</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityMemoryThresholdCard.Header" xml:space="preserve">
|
||||
<value>Memory usage (%)</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityMemoryThresholdInput.Header" xml:space="preserve">
|
||||
<value>Memory %</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityNetworkThresholdCard.Header" xml:space="preserve">
|
||||
<value>Network throughput (KB/s)</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityNetworkThresholdInput.Header" xml:space="preserve">
|
||||
<value>KB/s</value>
|
||||
</data>
|
||||
<data name="Awake_ActivitySampleIntervalCard.Header" xml:space="preserve">
|
||||
<value>Sample interval (s)</value>
|
||||
</data>
|
||||
<data name="Awake_ActivitySampleIntervalInput.Header" xml:space="preserve">
|
||||
<value>Seconds</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityInactivityTimeoutCard.Header" xml:space="preserve">
|
||||
<value>Inactivity timeout (s)</value>
|
||||
</data>
|
||||
<data name="Awake_ActivityInactivityTimeoutInput.Header" xml:space="preserve">
|
||||
<value>Timeout</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessSettingsExpander.Header" xml:space="preserve">
|
||||
<value>Process monitoring</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessSettingsExpander.Description" xml:space="preserve">
|
||||
<value>Stay awake while any monitored process is running</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessListCard.Header" xml:space="preserve">
|
||||
<value>Process names</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessListInput.Header" xml:space="preserve">
|
||||
<value>Process names (comma-separated)</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessListInput.PlaceholderText" xml:space="preserve">
|
||||
<value>e.g. notepad, chrome, myapp.exe</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessCheckIntervalCard.Header" xml:space="preserve">
|
||||
<value>Check interval (s)</value>
|
||||
</data>
|
||||
<data name="Awake_ProcessCheckIntervalInput.Header" xml:space="preserve">
|
||||
<value>Seconds</value>
|
||||
</data>
|
||||
<data name="Oobe_Awake.Title" xml:space="preserve">
|
||||
<value>Awake</value>
|
||||
<comment>Module name, do not loc</comment>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using ManagedCommon;
|
||||
@@ -96,6 +98,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public bool IsScreenConfigurationPossibleEnabled => ModuleSettings.Properties.Mode != AwakeMode.PASSIVE && IsEnabled;
|
||||
|
||||
public bool IsActivityConfigurationEnabled => ModuleSettings.Properties.Mode == AwakeMode.ACTIVITY && IsEnabled;
|
||||
|
||||
public bool IsProcessConfigurationEnabled => ModuleSettings.Properties.Mode == AwakeMode.PROCESS && IsEnabled;
|
||||
|
||||
public AwakeMode Mode
|
||||
{
|
||||
get => ModuleSettings.Properties.Mode;
|
||||
@@ -128,6 +134,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
|
||||
OnPropertyChanged(nameof(IsExpirationConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsActivityConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsProcessConfigurationEnabled));
|
||||
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
@@ -237,6 +245,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
|
||||
OnPropertyChanged(nameof(IsExpirationConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsActivityConfigurationEnabled));
|
||||
OnPropertyChanged(nameof(IsProcessConfigurationEnabled));
|
||||
}
|
||||
|
||||
public void RefreshModuleSettings()
|
||||
@@ -246,10 +256,114 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
OnPropertyChanged(nameof(IntervalHours));
|
||||
OnPropertyChanged(nameof(IntervalMinutes));
|
||||
OnPropertyChanged(nameof(ExpirationDateTime));
|
||||
OnPropertyChanged(nameof(ActivityCpuThresholdPercent));
|
||||
OnPropertyChanged(nameof(ActivityMemoryThresholdPercent));
|
||||
OnPropertyChanged(nameof(ActivityNetworkThresholdKBps));
|
||||
OnPropertyChanged(nameof(ActivitySampleIntervalSeconds));
|
||||
OnPropertyChanged(nameof(ActivityInactivityTimeoutSeconds));
|
||||
OnPropertyChanged(nameof(ProcessMonitoringList));
|
||||
OnPropertyChanged(nameof(ProcessCheckIntervalSeconds));
|
||||
OnPropertyChanged(nameof(TrackUsageEnabled));
|
||||
OnPropertyChanged(nameof(UsageRetentionDays));
|
||||
}
|
||||
|
||||
// Activity configuration bindables
|
||||
public uint ActivityCpuThresholdPercent
|
||||
{
|
||||
get => ModuleSettings.Properties.ActivityCpuThresholdPercent;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ActivityCpuThresholdPercent != value)
|
||||
{
|
||||
ModuleSettings.Properties.ActivityCpuThresholdPercent = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint ActivityMemoryThresholdPercent
|
||||
{
|
||||
get => ModuleSettings.Properties.ActivityMemoryThresholdPercent;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ActivityMemoryThresholdPercent != value)
|
||||
{
|
||||
ModuleSettings.Properties.ActivityMemoryThresholdPercent = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint ActivityNetworkThresholdKBps
|
||||
{
|
||||
get => ModuleSettings.Properties.ActivityNetworkThresholdKBps;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ActivityNetworkThresholdKBps != value)
|
||||
{
|
||||
ModuleSettings.Properties.ActivityNetworkThresholdKBps = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint ActivitySampleIntervalSeconds
|
||||
{
|
||||
get => ModuleSettings.Properties.ActivitySampleIntervalSeconds;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ActivitySampleIntervalSeconds != value)
|
||||
{
|
||||
ModuleSettings.Properties.ActivitySampleIntervalSeconds = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint ActivityInactivityTimeoutSeconds
|
||||
{
|
||||
get => ModuleSettings.Properties.ActivityInactivityTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ActivityInactivityTimeoutSeconds != value)
|
||||
{
|
||||
ModuleSettings.Properties.ActivityInactivityTimeoutSeconds = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process monitoring configuration bindables
|
||||
public string ProcessMonitoringList
|
||||
{
|
||||
get => string.Join(", ", ModuleSettings.Properties.ProcessMonitoringList);
|
||||
set
|
||||
{
|
||||
var processList = string.IsNullOrWhiteSpace(value)
|
||||
? new List<string>()
|
||||
: value.Split(',').Select(p => p.Trim()).Where(p => !string.IsNullOrEmpty(p)).ToList();
|
||||
|
||||
if (!ModuleSettings.Properties.ProcessMonitoringList.SequenceEqual(processList))
|
||||
{
|
||||
ModuleSettings.Properties.ProcessMonitoringList = processList;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint ProcessCheckIntervalSeconds
|
||||
{
|
||||
get => ModuleSettings.Properties.ProcessCheckIntervalSeconds;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ProcessCheckIntervalSeconds != value)
|
||||
{
|
||||
ModuleSettings.Properties.ProcessCheckIntervalSeconds = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
private bool _enabledGPOConfiguration;
|
||||
private AwakeSettings _moduleSettings;
|
||||
|
||||
Reference in New Issue
Block a user