mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-03 02:46:37 +01:00
Compare commits
2 Commits
dev/vanzue
...
leilzh/rot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba2d1fc3f5 | ||
|
|
f382b724a8 |
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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}";
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,12 @@ namespace Awake
|
||||
Logger.LogInfo($"OS: {Environment.OSVersion}");
|
||||
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
|
||||
|
||||
// Start background COM automation host (ROT) so any startup path exposes the automation surface.
|
||||
// Uses default moniker; could be extended with a --rotname parameter if needed later.
|
||||
var rotHost = new RotSingletonHost("Awake.Automation", () => new AwakeAutomation(), "AwakeAutomationRotThread");
|
||||
rotHost.Start();
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => rotHost.Stop();
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (sender, args) =>
|
||||
{
|
||||
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
|
||||
|
||||
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) }
|
||||
}
|
||||
Reference in New Issue
Block a user